﻿using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using log4net;
using System.IO.Compression;

namespace ResxCrunch.Business.Managers
{
    /// <summary>
    /// Encryption class
    /// Contains string encryption and randomization methods
    /// </summary>
    public class EncryptionMngr
    {
        #region Constants
        /// <summary>
        /// Logger declaration
        /// </summary>
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// Gets the reference to the object
        /// </summary>
        private static EncryptionMngr instance = new EncryptionMngr();

        /// <summary>
        /// Key used for encryption
        /// </summary>
        private static byte[] key = { 123, 217, 19, 11, 24, 26, 85, 45, 114, 184, 27, 162, 37, 112, 222, 209 };

        /// <summary>
        /// Vector used for encryption
        /// </summary>
        private static byte[] vector = { 146, 64, 191, 111, 23, 3, 113, 119, 231, 121, 251, 112, 79, 32, 114, 156 };
        #endregion Constants

        #region Attributes
        #endregion Attributes

        #region Initialization
        /// <summary>
        /// Prevents a default instance of the EncryptionMngr class from being created.
        /// </summary>
        private EncryptionMngr()
        {
        }

        /// <summary>
        /// Gets the instance of the class
        /// </summary>
        /// <value>The instance.</value>
        public static EncryptionMngr Instance
        {
            get
            {
                // Is created during type load - thus no further checking necessary...
                return instance;
            }
        }
        #endregion Initialization

        #region Public Methods
        #region Hash
        /// <summary>
        /// Hashes a string
        /// </summary>
        /// <param name="textToEncode">The text to encode.</param>
        /// <returns>Hashed string</returns>
        public string EncodeMD5(string textToEncode)
        {
            if (textToEncode == null)
            {
                textToEncode = string.Empty;
            }

            // Create a new instance of the MD5CryptoServiceProvider object.
            MD5 hasher = MD5.Create();

            // Convert the input string to a byte array and compute the hash.
            byte[] data = hasher.ComputeHash(Encoding.Default.GetBytes(textToEncode));

            // Create a new Stringbuilder to collect the bytes
            // and create a string.
            StringBuilder builder = new StringBuilder();

            // Loop through each byte of the hashed data 
            // and format each one as a hexadecimal string.
            for (int i = 0; i < data.Length; i++)
            {
                builder.Append(data[i].ToString("x2"));
            }

            // Return the hexadecimal string.
            return builder.ToString();
        }
        #endregion Hash

        #region Generate
        /// <summary>
        /// Generates a random string that has len length and consists of smallcaps letters
        /// </summary>
        /// <param name="len">Length of the generated string</param>
        /// <param name="readable">Indicates if the generated string should be easily readable and memorizable</param>
        /// <param name="avoidChars">Array of chars that should not be included in the generated string</param>
        /// <returns>A randomly generated string</returns>
        public string GenerateRandomString(int len, bool readable, char[] avoidChars)
        {
            if (!readable)
            {
                if (avoidChars == null)
                {
                    avoidChars = new char[] { };
                }

                // ToDo: implementation needed for readable password
                StringBuilder randomString = new StringBuilder();
                Random rnd = new Random((int)DateTime.Now.Ticks);

                char newChar;
                for (int i = 0; i < len; i++)
                {
                    do
                    {
                        newChar = (char)rnd.Next(97, 122);
                    }
                    while (avoidChars.Contains<char>(newChar));

                    randomString.Append(newChar);
                }

                return randomString.ToString();
            }
            else
            {
                return this.GenerateRandomString(len, !readable, avoidChars);
            }
        }

        /// <summary>
        /// Generates a random string from all available characters
        /// </summary>
        /// <param name="len">The length of the string that needs to be generated</param>
        /// <param name="readable">if set to <c>true</c> [readable].</param>
        /// <returns>A random string</returns>
        public string GenerateRandomString(int len, bool readable)
        {
            return this.GenerateRandomString(len, readable, null);
        }

        /// <summary>
        /// Generates a random string from all available characters which is not readable
        /// </summary>
        /// <param name="len">The length of the string that needs to be generated</param>
        /// <returns>A string composed of random characters</returns>
        public string GenerateRandomString(int len)
        {
            return this.GenerateRandomString(len, false, null);
        }

        /// <summary>
        /// Generates an encryption key.
        /// </summary>
        /// <returns></returns>
        public byte[] GenerateEncryptionKey()
        {
            // Generate a Key.
            RijndaelManaged rm = new RijndaelManaged();
            rm.GenerateKey();

            return rm.Key;
        }

        /// <summary>
        /// Generates a unique encryption vector
        /// </summary>
        /// <returns></returns>
        public byte[] GenerateEncryptionVector()
        {
            // Generate a Vector
            RijndaelManaged rm = new RijndaelManaged();
            rm.GenerateIV();

            return rm.IV;
        }
        #endregion Generate

        #region Encrypt/Decrypt
        /// <summary>
        /// Simple string encryption
        /// </summary>
        /// <param name="strToEncode">String to encode</param>
        /// <param name="key">A number, the key of the Encryption</param>
        /// <returns>Encrypted string</returns>
        public string SimpleEncrypt(string strToEncode, int key)
        {
            if (strToEncode != null)
            {
                StringBuilder res = new StringBuilder();
                char[] charArray = strToEncode.ToString(CultureInfo.InvariantCulture).ToCharArray();

                foreach (char c in charArray)
                {
                    res.Append((char)(((int)c) ^ key));
                }

                return res.ToString();
            }
            else
            {
                return String.Empty;
            }
        }

        /// <summary>
        /// Simple string decryption
        /// Exactly the same as Encrypt
        /// </summary>
        /// <param name="strToDecode">String to decode</param>
        /// <param name="key">Key of the description</param>
        /// <returns>Decrypted string</returns>
        public string SimpleDecrypt(string strToDecode, int key)
        {
            // Inverse of Encrypt = Encrypt
            return this.SimpleEncrypt(strToDecode, key);
        }

        /// <summary>
        /// Encrypt some text and return a string suitable for passing in a URL.
        /// </summary>
        /// <param name="textValue">The text value.</param>
        /// <returns></returns>
        public string EncryptAesToNumbersString(string textValue)
        {
            return ByteArrayToStringNumbers(EncryptAES(textValue));
        }

        /// <summary>
        /// Encrypt some text and return a string
        /// </summary>
        /// <param name="textValue">The text value.</param>
        /// <returns></returns>
        [Obsolete]
        public string EncryptAesToString(string textValue)
        {
            // ToDo: implement this
            return ByteArrayToString(EncryptAES(textValue));
        }

        /// <summary>
        /// Encrypt some text and return an UTF8 string
        /// </summary>
        /// <param name="textValue">The text value.</param>
        /// <returns></returns>
        [Obsolete]
        public string EncryptAesToUtf8String(string textValue)
        {
            // ToDo: implement this
            return ByteArrayToUtf8String(EncryptAES(textValue));
        }

        /// <summary>
        /// Encrypt some text and return an encrypted byte array.
        /// </summary>
        /// <param name="textValue">The text value.</param>
        /// <returns></returns>
        public byte[] EncryptAES(string textValue)
        {
            RijndaelManaged rm = new RijndaelManaged();
            ICryptoTransform encryptorTransform = rm.CreateEncryptor(key, vector);
            System.Text.UTF8Encoding utfEncoder = new System.Text.UTF8Encoding();

            // Translates our text value into a byte array.
            byte[] bytes = utfEncoder.GetBytes(textValue);

            // Used to stream the data in and out of the CryptoStream.
            MemoryStream memoryStream = new MemoryStream();

            #region Write the decrypted value to the encryption stream
            // We will have to write the unencrypted bytes to the stream,
            // then read the encrypted result back from the stream.
            CryptoStream cs = new CryptoStream(memoryStream, encryptorTransform, CryptoStreamMode.Write);

            cs.Write(bytes, 0, bytes.Length);
            cs.FlushFinalBlock();
            #endregion Write the decrypted value to the encryption stream

            #region Read encrypted value back out of the stream
            memoryStream.Position = 0;
            byte[] encrypted = new byte[memoryStream.Length];
            memoryStream.Read(encrypted, 0, encrypted.Length);
            #endregion Read encrypted value back out of the stream

            // Clean up.
            cs.Close();
            memoryStream.Close();

            return encrypted;
        }

        /// <summary>
        /// Decrypt a string built from numbers
        /// </summary>
        /// <param name="encryptedString">The encrypted string.</param>
        /// <returns></returns>
        public string DecryptAesNumbersString(string encryptedString)
        {
            return DecryptAes(NumbersStringToByteArray(encryptedString));
        }

        /// <summary>
        /// Decrypt an ASCII string
        /// </summary>
        /// <param name="encryptedString">The encrypted string.</param>
        /// <returns></returns>
        [Obsolete]
        public string DecryptAesString(string encryptedString)
        {
            // ToDo: implement this
            return DecryptAes(StringToByteArray(encryptedString));
        }

        /// <summary>
        /// Decrypt an UTF8 string
        /// </summary>
        /// <param name="encryptedString">The encrypted string.</param>
        /// <returns></returns>
        [Obsolete]
        public string DecryptAesUtf8String(string encryptedString)
        {
            // ToDo: implement this
            return DecryptAes(Utf8StringToByteArray(encryptedString));
        }

        /// <summary>
        /// Decryption when working with byte arrays.
        /// </summary>
        /// <param name="encryptedValue">The encrypted value.</param>
        /// <returns></returns>
        public string DecryptAes(byte[] encryptedValue)
        {
            try
            {
                RijndaelManaged rm = new RijndaelManaged();
                ICryptoTransform decryptorTransform = rm.CreateDecryptor(key, vector);
                System.Text.UTF8Encoding utfEncoder = new System.Text.UTF8Encoding();

                #region Write the encrypted value to the decryption stream
                MemoryStream encryptedStream = new MemoryStream();
                CryptoStream decryptStream = new CryptoStream(encryptedStream, decryptorTransform, CryptoStreamMode.Write);

                decryptStream.Write(encryptedValue, 0, encryptedValue.Length);
                decryptStream.FlushFinalBlock();
                #endregion Write the encrypted value to the decryption stream

                #region Read the decrypted value from the stream.
                encryptedStream.Position = 0;
                byte[] decryptedBytes = new byte[encryptedStream.Length];

                encryptedStream.Read(decryptedBytes, 0, decryptedBytes.Length);
                encryptedStream.Close();
                #endregion Read the decrypted value from the stream.

                return utfEncoder.GetString(decryptedBytes);
            }
            catch (Exception ex)
            {
                log.Error("Error Decrypting", ex);
                return null;
            }
        }
        #endregion Encrypt/Decrypt

        #region Transform
        /// <summary>
        /// Convert a string to a byte array.  NOTE: Normally we'd create a Byte Array from a string using an ASCII encoding (like so).
        /// System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
        /// return encoding.GetBytes(str);
        /// However, this results in character values that cannot be passed in a URL.  So, instead, I just
        /// lay out all of the byte values in a long string of numbers (three per - must pad numbers less than 100).
        /// </summary>
        /// <param name="str">The string that needs to be transformed into a byte array.</param>
        /// <returns></returns>
        public byte[] NumbersStringToByteArray(string str)
        {
            if (str.Length == 0)
            {
                throw new Exception("Invalid string value in StrToByteArray");
            }

            byte val;
            byte[] byteArr = new byte[str.Length / 3];
            int i = 0;
            int j = 0;

            do
            {
                val = byte.Parse(str.Substring(i, 3));
                byteArr[j++] = val;
                i += 3;
            }
            while (i < str.Length);

            return byteArr;
        }

        /// <summary>
        /// Convert a byte array to a string.  
        /// NOTE: Normally we'd create a Byte Array from a string using an ASCII encoding (like so).
        /// System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
        /// return encoding.GetBytes(str);
        /// However, this results in character values that cannot be passed in a URL.  So, instead, I just
        /// lay out all of the byte values in a long string of numbers (three per - must pad numbers less than 100).
        /// Normally the conversion would use an ASCII encoding in the other direction:
        ///      System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
        ///      return enc.GetString(byteArr); 
        /// </summary>
        /// <param name="byteArr">The byte arr.</param>
        /// <returns></returns>
        public string ByteArrayToStringNumbers(byte[] byteArr)
        {
            byte val;
            StringBuilder tempStr = new StringBuilder();

            for (int i = 0; i <= byteArr.GetUpperBound(0); i++)
            {
                val = byteArr[i];

                tempStr.AppendFormat("{0:000}", val);
            }

            return tempStr.ToString();
        }

        /// <summary>
        /// Converts ASCII string to byte array.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        [Obsolete]
        public byte[] StringToByteArray(string text)
        {
            System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
            return encoding.GetBytes(text);
        }

        /// <summary>
        /// Converts UTF8 string to byte array.
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        [Obsolete]
        public byte[] Utf8StringToByteArray(string text)
        {
            System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
            return encoding.GetBytes(text);
        }

        /// <summary>
        /// Converts byte array to ASCII string
        /// </summary>
        /// <param name="byteArr"></param>
        /// <returns></returns>
        [Obsolete]
        public string ByteArrayToString(byte[] byteArr)
        {
            System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
            return enc.GetString(byteArr);
        }

        /// <summary>
        /// Converts byte array to UTF8 string
        /// </summary>
        /// <param name="byteArr"></param>
        /// <returns></returns>
        [Obsolete]
        public string ByteArrayToUtf8String(byte[] byteArr)
        {
            System.Text.UTF8Encoding enc = new System.Text.UTF8Encoding();
            return enc.GetString(byteArr);
        }
        #endregion Transform

        #region Compress/Deflate
        /// <summary>
        /// Compresses the specified text.
        /// Note: does not work well with small texts
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        public string Compress(string text)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(text);
            MemoryStream ms = new MemoryStream();
            using (GZipStream zip = new GZipStream(ms, CompressionMode.Compress, true))
            {
                zip.Write(buffer, 0, buffer.Length);
            }

            ms.Position = 0;
            MemoryStream outStream = new MemoryStream();

            byte[] compressed = new byte[ms.Length];
            ms.Read(compressed, 0, compressed.Length);

            byte[] gzBuffer = new byte[compressed.Length + 4];
            System.Buffer.BlockCopy(compressed, 0, gzBuffer, 4, compressed.Length);
            System.Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gzBuffer, 0, 4);
            return Convert.ToBase64String(gzBuffer);
        }

        /// <summary>
        /// Decompresses the specified compressed text.
        /// </summary>
        /// <param name="compressedText">The compressed text.</param>
        /// <returns></returns>
        public string Decompress(string compressedText)
        {
            byte[] gzBuffer = Convert.FromBase64String(compressedText);
            using (MemoryStream ms = new MemoryStream())
            {
                int msgLength = BitConverter.ToInt32(gzBuffer, 0);
                ms.Write(gzBuffer, 4, gzBuffer.Length - 4);

                byte[] buffer = new byte[msgLength];

                ms.Position = 0;
                using (GZipStream zip = new GZipStream(ms, CompressionMode.Decompress))
                {
                    zip.Read(buffer, 0, buffer.Length);
                }

                return Encoding.UTF8.GetString(buffer);
            }
        }

        #endregion Compress/Deflate
        #endregion Public Methods
    }
}
